﻿using System;
using System.Diagnostics;
using System.Linq.Expressions;
using System.Reflection;

namespace NanoMvvm
{
    public class WeakActionContainer<TParam> : IWeakExecutable<TParam>, IEquatable<WeakActionContainer<TParam>>, IEquatable<Action<TParam>>
    {
        private delegate void OpenAction<T>(object instance, T param);

        private readonly Action<TParam> staticInvoker;
        private readonly OpenAction<TParam> instanceInvoker;
        private readonly MethodInfo instanceMethodInfo;
        private readonly WeakReference<object> target;

        private readonly bool isStatic;


        public WeakActionContainer(Action<TParam> action)
        {
            if (action == null) throw new ArgumentNullException("action");
            if (action.Method.DeclaringType.IsValueType) throw new ArgumentException("Declaring type must be a reference type.");

            if(action.Target == null)
            {
                Debug.Assert(action.Method.IsStatic);

                isStatic = true;
                staticInvoker = action;
            }
            else
            {
                Debug.Assert(!action.Method.IsStatic);

                target = new WeakReference<object>(action.Target);

                instanceInvoker = CreateOpenActionDelegate(action);

                instanceMethodInfo = action.Method;
            }
        }

        public void Execute(TParam parameter)
        {
            if (isStatic)
            {
                staticInvoker(parameter);
            }
            else if (target.IsTargetAlive)
            {
                instanceInvoker(target.Target, parameter);
            }
            else
            {
                throw new InvalidOperationException("Delegate target instance garbage collected before call.");
            }
        }

        public bool IsTargetAlive
        {
            get { return isStatic || target.IsTargetAlive; }
        }

        private static OpenAction<T> CreateOpenActionDelegate<T>(Action<T> action)
        {
            Debug.Assert(action != null);
            Debug.Assert(!action.Method.IsStatic);

            MethodInfo method = action.Method;
            Type type = typeof (T);

            ParameterExpression instanceExpression = Expression.Parameter(typeof(object));
            ParameterExpression paramExpression = Expression.Parameter(type);

            Expression<OpenAction<T>> expression = Expression.Lambda<OpenAction<T>>(
                Expression.Call(
                    Expression.Convert(instanceExpression, action.Method.DeclaringType),
                    method,
                    paramExpression
                ),
                instanceExpression,
                paramExpression
            );

            return expression.Compile();
        }

        public override int GetHashCode()
        {
            return isStatic ? staticInvoker.GetHashCode() : instanceInvoker.GetHashCode();
        }

        public override bool Equals(object other)
        {
            if (ReferenceEquals(null, other)) return false;

            var wac = other as WeakActionContainer<TParam>;
            if (!ReferenceEquals(null, wac))
            {
                return Equals(wac);
            }

            var a = other as Action<TParam>;
            if (!ReferenceEquals(null, a))
            {
                return Equals(a);
            }

            return false;
        }

        public bool Equals(Action<TParam> other)
        {
            if (ReferenceEquals(null, other)) return false;

            if (other.Target == null)
            {
                Debug.Assert(other.Method.IsStatic);

                return isStatic && other.Method.Equals(staticInvoker.Method);
            }

            Debug.Assert(!other.Method.IsStatic);

            return !isStatic && target.IsTargetAlive &&
                   other.Method.Equals(instanceMethodInfo) &&
                   ReferenceEquals(other.Target, target.Target);
        }

        public bool Equals(WeakActionContainer<TParam> other)
        {
            if (ReferenceEquals(null, other)) return false;

            if (other.isStatic)
            {
                return isStatic && other.staticInvoker.Equals(staticInvoker);
            }

            return !isStatic && target.IsTargetAlive && //other.target.IsTargetAlive &&
                   other.instanceMethodInfo.Equals(instanceMethodInfo) &&
                   ReferenceEquals(other.target.Target, target.Target);
        }
    }
}